<!--
  * @Description 上传组件
  * @Author 莫发达
  * @create 2024-09-02 13:48 周一
-->
<script setup lang="ts">
import {computed, defineProps, onMounted, ref, watch} from 'vue'
import {Plus} from '@element-plus/icons-vue'
import type {
  IFileInfo,
  IRawFile,
  IUploadFile,
  ListType,
  UploadHeaders,
  UploadMethod
} from './types.ts'
import type {
  UploadData,
  UploadInstance, UploadRequestOptions
} from "element-plus";
import ImportFile from './components/ImportFile.vue'// 导入文件预览
import CardFile from "./components/CardFile.vue";
import {useUploadHooks} from "./upload-hooks.ts"; // 上传钩子
import SvgIcon from "@/components/svg-icon/index.vue";
import {
  convertPropFileList
} from "@/components/base-upload/util.ts";
import {deepClone} from "sf-utils2";
import {isEqual} from "lodash-es";
import minioClient from "./minio/client.ts";
import cfCommonApi from "@/api/modules/cf-common";
import useUserStore from "@store/use-user-store.ts";


interface IProps {
  /**
   * 请求 URL
   */
  action?: string;

  /**
   * 设置上传的请求头部
   */
  headers?: UploadHeaders;

  /**
   * 设置上传请求方法，默认使用 'post'
   */
  method?: UploadMethod;

  /**
   * 是否支持多选文件，默认值为 false
   */
  multiple?: boolean;

  /**
   * 上传时附带的额外参数，可以是对象或函数
   * @version 从 v2.3.13 支持 Awaitable 数据和函数
   */
  data?: UploadData | (() => UploadData | Promise<UploadData>);

  /**
   * 上传的文件字段名，默认值为 'file'
   */
  name?: string;

  /**
   * 支持发送 cookie 凭证信息，默认值为 false
   */
  withCredentials?: boolean;

  /**
   * 是否显示已上传文件列表，默认值为 true
   */
  showFileList?: boolean;

  /**
   * 是否启用拖拽上传，默认值为 false
   */
  drag?: boolean;

  /**
   * 接受上传的文件类型， 建议使用 英文逗号分割 例如：'.png,.jpg'
   * @remark thumbnail-mode 模式下此参数无效
   */
  accept?: string;

  /**
   * 原生属性 crossorigin
   */
  crossorigin?: '' | 'anonymous' | 'use-credentials';

  /**
   * 点击文件列表中已上传的文件时的钩子
   */
  onPreview?: (file: IUploadFile) => void;

  /**
   * 文件列表移除文件时的钩子
   */
  onRemove?: (file: IUploadFile, fileList: IUploadFile[]) => void | boolean | Promise<void | boolean>;

  /**
   * 文件上传成功时的钩子
   */
  onSuccess?: (response: {
    code: number,
    data: IFileInfo,
    message: string
  }, file: IUploadFile, fileList: IUploadFile[]) => void;

  /**
   * 文件上传之前的钩子，参数为上传的文件，若返回 false 或者返回 Promise 且被 reject，则停止上传
   * @param response
   * @param file
   * @param fileList
   */
  beforeSuccess?: (response: {
    code: number,
    data: IFileInfo,
    message: string
  }, file: IUploadFile, fileList: IUploadFile[]) => void;

  /**
   * 文件上传失败时的钩子
   */
  onError?: (error: any, file: IUploadFile, fileList: IUploadFile[]) => void;

  /**
   * 文件上传时的钩子
   */
  onProgress?: (event: ProgressEvent, file: IUploadFile, fileList: IUploadFile[]) => void;

  /**
   * 文件状态改变时的钩子，添加文件、上传成功和上传失败时都会被调用
   */
  onChange?: (file: IUploadFile, fileList: IUploadFile[]) => void;

  /**
   * 当超出限制时，执行的钩子函数
   */
  onExceed?: (files: File[], fileList: IUploadFile[]) => void;

  /**
   * 文件超时限制时，是否需要替换
   */
  exceedReplace?: boolean

  /**
   * 上传文件之前的钩子，参数为上传的文件
   * @remark 若返回 false 或者返回 Promise 且被 reject，则停止上传
   */
  beforeUpload?: (file: File) => boolean | void | Promise<boolean | void>;

  /**
   * 删除文件之前的钩子，参数为上传的文件和文件列表
   * @remark 若返回 false 或者返回 Promise 且被 reject，则停止删除
   */
  beforeRemove?: (file: IUploadFile, fileList: IUploadFile[]) => boolean | void | Promise<boolean | void>;

  /**
   * 默认上传文件列表
   */
  fileList?: Array<IUploadFile | IFileInfo>;

  /**
   * 文件列表的类型，默认为 'text'
   * text - 文字
   * picture - 图片
   * card - 卡片样式
   * import - 导入样式
   */
  template?: "text" | "picture" | "card" | "import";

  /**
   * 文件列表的类型，默认为 'text'
   */
  listType?: ListType;

  /**
   * 是否自动上传文件，默认值为 true
   */
  autoUpload?: boolean;

  /**
   * 覆盖默认的 Xhr 行为，允许自行实现上传文件的请求
   * @remark 请参考 ajaxUpload 方法
   */
  httpRequest?: (options: any) => void;

  /**
   * 是否禁用上传，默认值为 false
   */
  disabled?: boolean;

  /**
   * 允许上传文件的最大数量
   */
  limit?: number;

  /**
   * 文件尺寸限制，单位 MB
   */
  fileSize?: number;

  /**
   * 提示文字
   */
  placeholder?: string;

  /**
   * 提示文字
   */
  tip?: string;

  /**
   * 是否启用 minio 上传，默认值为 true
   */
  minio?: boolean;

  /**
   * 文件上传模块 取枚举值 $enums.pathModule
   */
  module?: string;

  /**
   * minio 桶类型，默认值为 'public'
   */
  bucketType?: 'public' | 'private'
}

const _userStore = useUserStore()

const props = withDefaults(defineProps<IProps>(), {
  template: 'text',
  autoUpload: true,
  showFileList: true,
  accept: '.xls,.xlsx',
  fileSize: 10,
  bucketType: 'private',
  module: 'common',
  minio: true
})
const emits = defineEmits<{
  (e: 'update:fileList', fileList: IUploadFile[]): void
}>()

/**
 * 上传组件实例
 */
const uploadRef = ref<UploadInstance>()

/**
 * 文件列表
 */
const _fileList = ref<IUploadFile[]>([])

/**
 * 文件列表类型
 */
const _listType = computed(() => {
  const {template, listType} = props

  switch (template) {
    case 'text':
    case 'import':
      return 'text'
    case 'card':
    case 'picture':
      return 'picture-card'
    default:
      return listType
  }
})

/**
 * 上传文件之前的钩子
 */
const {onBeforeUpload, onFileSuccess, onExceed, onRetry, onPreview, onDelete} =
  useUploadHooks(props, uploadRef);

/**
 * 文件发生变化时触发
 * @param uploadFile
 * @param uploadFiles
 */
function onChange(uploadFile: IUploadFile, uploadFiles: IUploadFile[]) {
  emits('update:fileList', deepClone(_fileList.value))
  props.onChange?.(uploadFile, uploadFiles)
}

/**
 * 上传文件
 * @param options
 */
function minioRequest(options: UploadRequestOptions) {
  const {module, bucketType} = props

  minioClient.putObject({
    file: options.file as IRawFile,
    module: module,
    onProgress: options.onProgress,
    onError: options.onError,
    onSuccess: options.onSuccess,
    bucketType: bucketType,
    onReading: (event) => {
      const file = _fileList.value.find((file) => file.uid === options.file.uid)
      if (!file) return

      file.status = 'reading';
      file.percentage = Math.round(event.percent);
    }
  })
}

function beforeRemove(file: IUploadFile, fileList: IUploadFile[]) {
  props.beforeRemove?.(file, fileList)

  // 取消上传
  return minioClient.abort(file.uid)
}

onMounted(async () => {
  if (!props.minio || minioClient.isInitiated) return
  // 初始化文件列表
  const {data} = await cfCommonApi.getMinioBucketName();
  minioClient.setBucket(data)
})

/**
 * 监听文件列表变化
 */
watch(() => props.fileList, async (fileList) => {
  // 如果文件列表相同，则不进行处理
  if (isEqual(_fileList.value, fileList)) return

  _fileList.value = convertPropFileList(fileList, (index, file) => {
    // 回调函数，用于处理文件列表
    const uploadFile = _fileList.value[index]
    if (uploadFile.file.fileName !== file.fileName) return

    uploadFile.file = file
    uploadFile.url = file.previewUrl
  })
  emits('update:fileList', deepClone(_fileList.value))
}, {immediate: true, deep: true})

defineExpose({
  /**
   * 上传组件实例
   */
  get uploadRef(): UploadInstance {
    return uploadRef.value
  },
  onRetry,
  /**
   * 文件列表
   */
  get fileList(): IFileInfo[] {
    return _fileList.value.map((file) => file.file).filter(Boolean)
  },
  /**
   * 获取上传列表
   */
  get uploadList() {
    return _fileList.value
  },
  /**
   * 是否正在上传
   */
  get isUploading(): boolean {
    return _fileList.value.some((file) => ['uploading', 'ready'].includes(file.status))
  },
})
</script>

<template>
  <el-upload
    ref="uploadRef"
    v-bind="props"
    v-model:file-list="_fileList"
    :headers="{'x-user-token': _userStore?.token,...headers}"
    class="select-none"
    :class="[`upload-${props.template}`,{'upload--exceed': props.limit && _fileList.length >= props.limit}]"
    :on-success="onFileSuccess"
    :on-exceed="onExceed"
    :on-change="onChange"
    :before-remove="beforeRemove"
    :before-upload="onBeforeUpload"
    :list-type="_listType"
    :http-request="minio? minioRequest : httpRequest"
  >
    <slot name="default">
      <!--导入样式-->
      <template v-if="props.template === 'import'">
        <!--导入模式-->
        <div
          class="min-h-[140px] w-full flex flex-col gap-2 justify-center items-center"
        >
          <SvgIcon
            svg="upload/cloud"
            size="48"
          />
          <div class="uploader__import__placeholder text-center text-text-tertiary text-sm">
            将文件拖到此处，或<strong class="text-primary">点击上传</strong>
            <br>
            <span class="text-xs">{{ props.placeholder ?? "仅支持扩展名：xls、xlsx" }}</span>
          </div>
        </div>
      </template>

      <!--普通按钮文字-->
      <template v-else-if="props.template === 'text'">
        <button
          class="text-primary flex items-center gap-1 mr-auto text-sm font-bold"
          type="button"
        >
          <SvgIcon
            svg="upload/upload"
            size="14"
          />
          <span>{{ props.placeholder ?? "点击上传" }}</span>
        </button>
      </template>

      <!--身份证之类的上传-->
      <template v-else-if="props.template === 'card'">
        <button
          class="w-full mr-auto relative flex items-center justify-center rounded-xl"
          type="button"
        >
          <img
            src="@/assets/imgs/upload/portrait.svg"
            alt=""
          >

          <span
            class="absolute flex flex-col items-center gap-1 text-text-primary text-sm font-bold"
          >
            <Plus class="bg-primary w-[21px] h-[21px] rounded-full p-1 text-white" />

            <span>{{ props.placeholder ?? "点击上传" }}</span>
          </span>
        </button>
      </template>

      <!--图片上传-->
      <template v-else-if="props.template === 'picture'">
        <button
          type="button"
          class="w-full h-full mr-auto relative flex flex-col gap-1 items-center justify-center rounded-md bg-primary/5 text-primary text-md"
        >
          <Plus class="w-6 h-6" />

          <span class="text-sm">{{ props.placeholder ?? "上传图片" }}</span>
        </button>
      </template>
    </slot>

    <!--文件预览-->
    <template #file="{file,index}">
      <slot
        name="file"
        v-bind="{file, index, actions: { onDelete:onDelete, onRetry:onRetry, onPreview:onPreview }}"
      >
        <!--身份证卡片、图片-->
        <CardFile
          v-if="[ 'card', 'picture' ].includes(props.template)"
          :file="file"
          :index="index"
          @delete="onDelete(file)"
          @retry="onRetry(file)"
          @preview="onPreview(file)"
        />
        <!--导入文件、普通文本-->
        <ImportFile
          v-else
          :file="file"
          :index="index"
          @delete="onDelete(file)"
          @retry="onRetry(file)"
          @preview="onPreview(file)"
        />
      </slot>
    </template>

    <template #tip>
      <slot name="tip">
        <!--  fix: 当tip为空的时候，应该不需要占据空间的   -->
        <div :class="[`el-upload__tip text-sm text-text-tertiary`, props.tip && 'mt-1']">
          {{ props.tip }}
        </div>
      </slot>
    </template>
  </el-upload>
</template>

<style scoped lang="less">
// 上传样式 卡片
.upload-card {
  :deep(.ijep-upload-list__item),
  :deep(.ijep-upload--picture-card) {
    width: 285px;
    height: 180px;
    border: none;
  }
}

// 拖放样式
:deep(.ijep-upload-dragger) {
  padding: 0;
}

// 上传样式 图片
:deep(.ijep-upload--text) {
  width: 100%;
}

// 卡片超出限制
.upload-card.upload--exceed,
.upload-picture.upload--exceed {
  :deep(.ijep-upload ) {
    display: none;
  }
}

// 样式修复 上传列表为空时
:deep(.ijep-upload-list):empty {
  margin: 0;
}
</style>
